/** * Copyright (C) 2008 Abiquo Holdings S.L. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.abiquo.apiclient.stream; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Throwables.getStackTraceAsString; import static org.atmosphere.wasync.Event.CLOSE; import static org.atmosphere.wasync.Event.MESSAGE; import java.io.Closeable; import java.io.IOException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import org.atmosphere.wasync.ClientFactory; import org.atmosphere.wasync.Function; import org.atmosphere.wasync.Request.METHOD; import org.atmosphere.wasync.Request.TRANSPORT; import org.atmosphere.wasync.Socket; import org.atmosphere.wasync.impl.AtmosphereClient; import org.atmosphere.wasync.impl.AtmosphereRequest; import com.abiquo.event.json.module.AbiquoModule; import com.abiquo.event.model.Event; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; import com.google.common.base.Throwables; import com.ning.http.client.AsyncHttpClient; import com.ning.http.client.AsyncHttpClientConfig; import com.ning.http.client.Realm; public class StreamClient implements Closeable { private static final Logger LOG = Logger.getLogger("abiquo.stream"); private final String endpoint; private final String username; private final String password; private final SSLConfiguration sslConfiguration; private final Consumer<Event> consumer; private boolean reconnect = false; private int reconnectAttempts = 0; private int pauseBeforeReconnectInSeconds = 0; private final Runnable beforeReconnection; private final Runnable afterReconnection; // no configs private final ObjectMapper json; private AsyncHttpClient asyncClient; private Socket socket; private AtomicBoolean manuallyClosing = new AtomicBoolean(false); private Executor reconnectionExecutor = Executors.newCachedThreadPool(); // Do not use directly. Use the builder. private StreamClient(final String endpoint, final String username, final String password, final SSLConfiguration sslConfiguration, final Consumer<Event> consumer, final Runnable beforeReconnection, final Runnable afterReconnection, final boolean reconnect, final int reconnectAttempts, final int pauseBeforeReconnectInSeconds) { this.endpoint = checkNotNull(endpoint, "endpoint cannot be null"); this.username = checkNotNull(username, "username cannot be null"); this.password = checkNotNull(password, "password cannot be null"); this.consumer = checkNotNull(consumer, "consumer cannot be null"); this.sslConfiguration = sslConfiguration; this.reconnect = checkNotNull(reconnect); if (this.reconnect) { this.reconnectAttempts = checkNotNull(reconnectAttempts, "reconnect attempts cannot be null if reconnection has been enabled"); this.pauseBeforeReconnectInSeconds = checkNotNull(pauseBeforeReconnectInSeconds, "pause seconds before reconnect cannot be null if reconnection has been enabled"); this.beforeReconnection = beforeReconnection; this.afterReconnection = afterReconnection; } else { this.beforeReconnection = null; this.afterReconnection = null; } json = new ObjectMapper() .setAnnotationIntrospector( // new AnnotationIntrospectorPair(new JacksonAnnotationIntrospector(), new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()))) // .registerModule(new AbiquoModule()); } public void connect() throws IOException { checkState(socket == null, "the client is already listening to events"); checkState(asyncClient == null, "the client is already listening to events"); LOG.fine("Connecting to " + endpoint + "..."); AtmosphereClient client = ClientFactory.getDefault().newClient(AtmosphereClient.class); AtmosphereRequest request = client.newRequestBuilder() // .method(METHOD.GET) // .uri(endpoint + "?Content-Type=application/json") // .transport(TRANSPORT.SSE) // .transport(TRANSPORT.LONG_POLLING).build(); AsyncHttpClientConfig.Builder config = new AsyncHttpClientConfig.Builder(); config.setRequestTimeoutInMs(-1); config.setIdleConnectionTimeoutInMs(-1); if (sslConfiguration != null) { config.setHostnameVerifier(sslConfiguration.hostnameVerifier()); config.setSSLContext(sslConfiguration.sslContext()); } config.setRealm(new Realm.RealmBuilder() // .setPrincipal(username) // .setPassword(password) // .setUsePreemptiveAuth(true) // .setScheme(Realm.AuthScheme.BASIC) // .build()); asyncClient = new AsyncHttpClient(config.build()); socket = client.create(client.newOptionsBuilder().runtime(asyncClient).build()); socket.open(request); configure(); LOG.fine("Connected!"); } private void configure() { socket.on(MESSAGE, new Function<String>() { @Override public void on(final String rawEvent) { try { Event event = json.readValue(rawEvent, Event.class); consumer.accept(event); } catch (IOException ex) { LOG.warning(String.format("Unexpected error processing event: %s\n%s", ex.getMessage(), getStackTraceAsString(ex))); } } }) .on(CLOSE, new Function<String>() { @Override public void on(final String rawEvent) { if (!manuallyClosing.get() && reconnect) { reconnectionExecutor.execute(new Runnable() { @Override public void run() { try { if (beforeReconnection != null) { beforeReconnection.run(); } LOG.warning("Connection lost, going to reconnect"); reconnect(); LOG.fine("Reconnected"); if (afterReconnection != null) { afterReconnection.run(); } } catch (IOException e) { throw Throwables.propagate(e); } } }); } } }); } private void reconnect() throws IOException { int retry = 0; while (retry < reconnectAttempts) { try { closeConnection(); connect(); return; } catch (IOException e) { retry++; try { Thread.sleep(pauseBeforeReconnectInSeconds * 1000); } catch (InterruptedException e1) { throw Throwables.propagate(e1); } } } LOG.severe("Reconnection failed"); closeConnection(); } @Override public synchronized void close() throws IOException { try { manuallyClosing.set(true); closeConnection(); } finally { manuallyClosing.set(false); } } private synchronized void closeConnection() throws IOException { LOG.fine("Disconnecting..."); if (asyncClient != null) { asyncClient.close(); } if (socket != null) { socket.close(); } asyncClient = null; socket = null; LOG.fine("Disconnected!"); } public static Builder builder() { return new Builder(); } public static class Builder { private String endpoint; private String username; private String password; private SSLConfiguration sslConfiguration; private Consumer<Event> consumer; private boolean reconnect = false; private int reconnectAttempts = 10; private int pauseBeforeReconnectInSeconds = 5; private Runnable beforeReconnect; private Runnable afterReconnect; public Builder endpoint(final String endpoint) { this.endpoint = endpoint; return this; } public Builder credentials(final String username, final String password) { this.username = username; this.password = password; return this; } public Builder sslConfiguration(final SSLConfiguration sslConfiguration) { this.sslConfiguration = sslConfiguration; return this; } public Builder consumer(final Consumer<Event> consumer) { this.consumer = consumer; return this; } public Builder reconnect(final boolean reconnect) { this.reconnect = reconnect; return this; } public Builder reconnectAttempts(final int attempts) { this.reconnectAttempts = attempts; return this; } public Builder pauseBeforeReconnectInSeconds(final int seconds) { this.pauseBeforeReconnectInSeconds = seconds; return this; } public Builder beforeReconnect(final Runnable beforeReconnect) { this.beforeReconnect = beforeReconnect; return this; } public Builder afterReconnect(final Runnable afterReconnect) { this.afterReconnect = afterReconnect; return this; } public StreamClient build() { return new StreamClient(endpoint, username, password, sslConfiguration, consumer, beforeReconnect, afterReconnect, reconnect, reconnectAttempts, pauseBeforeReconnectInSeconds); } } public static interface SSLConfiguration { /** * Provides the SSLContext to be used in the SSL sessions. */ public SSLContext sslContext(); /** * Provides the hostname verifier to be used in the SSL sessions. */ public HostnameVerifier hostnameVerifier(); } }